关键词:go, channel
goroutine 是一个轻量级的执行单元,相比线程开销更小,完全由 Go 语言负责调度,是 Go 支持并发的核心。开启一个 goroutine 非常简单:
|
|
不要通过共享来通信,而要通过通信来共享。
goroutine
运行在相同的地址空间,因此访问共享内存必须做好同步,golang
是通过channel
来实现的通信的。
unbufered channel
Channel
可以分为两种类型,一种是buffer
为0的,一种是buffer
不为零的。
|
|
当channel
的 buffer
为零时,由于没有缓存,因此,可以非常容易做到Goroutines的同步.
send
语句用来往Channel
中发送数据, 如ch <- 3
receive
语句<-ch
用来从channel ch
中接收数据,这个表达式会一直被block
,直到有数据可以接收。
对unbuffered channel ch
进行读操作v := <- ch
时(即进行receive
时),会一直阻塞,直到有数据send
到ch
中(即当有ch <- value
)时才会发生通讯。
即:如果没有设置容量,或者容量设置为0, 说明Channel
没有缓存,只有sender
和receiver
都准备好了后它们的通讯才会发生, 实现goroutine
间的同步(main
函数自身就是一个 goroutine
)
示例代码如下:
|
|
这个示例代码有两个goroutine
, 其中一个是main
函数的,一个是go
出来的函数,通过返回的结果可以看出,main
携程进行到<- c
时就阻塞了,因为此时c
里时空的,没有向c``中发送数据,转而去执行另一个goroutine
, 所以会打印出“goroutine message”
,之后进行了一次sleep
,交出了这个goroutine
的控制权,回到main
这个goroutine
中,发现c
还是没有接受到数据,因此这个main
的goroutine
依旧阻塞着,过了一秒后,控制权重新回到另一个goroutine
, 此时发现有数据写入c
了,二者发生了通信,实现了同步,因此打印出了"goroutine message 2"
和"main function message"
, 由于此时main
这个goroutine
已经结束了,因此整个程序也结束了,所以另一个goroutine
的后续操作就没有继续执行。
buffered channel
如果 channel 的容量不是 0,此类 channel 称之为 buffered channel ,buffered channel 在消息写入个数 未达到容量的上限之前不会阻塞 ,一旦写入消息个数超过上限,下次输入将会阻塞,直到 channel 有位置可以再写入。
示例代码
|
|
将 dealSlice
go
出去后,由于c
的容量是3,因此在c
达到3的容量前,是不会阻塞的,此时另一个main
的 goroutine
正在Sleep
,一旦当c
的容量到了3之后,这个goroutine
就阻塞了,直到main
的sleep
走完,并且完成从c
中取出一个数据,一旦从c
中取出一个数据,这个时候c
的大小就小于3了,此时不再阻塞,于是dealSlice这个
goroutine 又开始运行(但是由于两个
goroutine都没有阻塞了此时,所以二者谁先
append进去是不确定的)但是一旦
dealSlice完成后,就使用
close将这个
c关闭了,此时无法再对
c写入任何的数据了,一旦再有数据写入就会引发
panic,但是依旧可以从
c中读取数据,直到读取完毕,因此,会看到如输出的结果一样的东西。这就是
buggered channel的作用,有
buffer`,所以在达到容量上限前是不会阻塞的。
select
— 使用 select 读取多个 channel
当同时又多个channel时,select 语句可以从多个可读的 channel 中随机选取一个执行,注意是 随机选取。但是这个随机选取是有条件的,这个channel是不阻塞的,如果channel阻塞,他会随机选择其他的不阻塞的一个。
我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字
select
,通过select
可以监听channel上的数据流动。
select
默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
代码实例
|
|
根据代码和输出的情况来看,新起的goroutine
一开始就会阻塞,因为c
里没有数据可以取出,因此,阻塞在这里等待其他goroutine
里有数据输入到c
中,而fibonacci
所在的main goroutine
在select
中选择case
,两个随机选择,但是<-quit
阻塞,因此,只能选择不阻塞的c <- x
, 这样两个goroutine``就都可以进行下去,直到func
这个 goroutine
的for
循环结束,此时fibonacci
那里的数据已经被func
全部取完了,fibonacci
又阻塞了,func
这时走到了quit <- 0
,阻塞到这里,等待quit
有人接收,正好此时select
发现case2
正好可以实现同步,func
这个goroutine
先结束,主程序main
的goroutine
后结束(因为func
只有一个channel
接收数据,而main
这个goroutine
除了和其对接的 <- quit
还有一个打印操作,所以,最后结束点落到main
上,让程序正好OK
,如果func
在quit<-0
后,还有一些其他的阻塞操作,那么,可能就无法执行,因为此时主程序的goroutine
已经结束。示例如下:
|
|
正如我们预测的那样。
在select
里面还有default语法,select
其实就是类似switch
的功能,default
就是当监听的channel
都没有准备好的时候,默认执行的(select
不再阻塞等待channel
)。
|
|
timeout
超时的处理
有时候可能会出现goroutine超时的情况,这个时候我们可以使用 select 设置超时的处理,一旦超时超过一段时间,我们就使用 select 选择处理的case,保证程序的正常进行。代码实例如下:
|
|
我们可以看到此时一共有两个goroutine
, 一个是main
的,一个是go
出的func
的,func
是一个无限循环,由于两个case
在5秒内都不会有任何反应,因此会在select
这里阻塞5秒,而main
中是从o
这个channel
中取数据,没有数据的流入因此也阻塞到这里,此时超时就发挥作用了,这个time.After()
的返回值是一个channel
,当5秒过去后,这个case
不阻塞了,于是向下执行,先是打印出 timeout
这个字符,然后 向o
这个channel
写入数据,与此同时 <-o
这个阻塞也解除,func
跳出循环结束函数,main
结束此goroutine
。
close
关闭 channel
Channel 可以被关闭 close
,channel 关闭之后仍然可以读取,但是向被关闭的 channel send 会 panic。如果 channel 关闭之前有值写入,关闭之后将依次读取 channel 中的消息,读完完毕之后再次读取将会返回 channel 的类型的 zero value:
|
|
输出 1 2 3 0 0 0 ,0 是 int channel c 的 zero value。
|
|
c 可以进行 range 迭代,如果 channel 没有被关闭 range 会一直等待 channel,但是关闭 channel 之后可以隐式的中断 range 的迭代。
如何判断channel
的关闭与否呢?
Go 提供了 ok 表达式来判断 channel 的关闭状态。
|
|
如果 channel 是关闭状态,ok 是 false,value 是 channel 的 zero value,否则 ok 是 true 表示 channel 未关闭,value 表示 channel 中的值。